"""
PyS60 FlickrAPI - Flickr API implementation for Python for Series 60
Copyright (C) 2006 Teemu Harju

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

PyS60 FlickrAPI is based on Beej's awesome Python Flickr API.
http://beej.us/flickr/flickrapi/

teemu.harju@gmail.com

$Author: teemu.harju $
$Rev: 26 $
$LastChangedDate: 2007-07-21 05:35:47 +0000 (Sat, 21 Jul 2007) $
"""

import sys
import md5
import string
import urllib
import httplib
import mimetools
import os.path
import os
import time
import socket

from __main__ import APPPATH

import xmllib

########################################################################
# Exceptions
########################################################################

class UploadException(Exception):
	pass

class FlickrException(Exception):
	def __init__(self, msg):
		self.msg = msg

	def __str__(self):
		return repr(self.msg)

########################################################################
# Class method wrapper (Python recipe 52304) 
########################################################################

class Callable:
	"""Callable -- simple wrapper for class methods
	
	PyS60 does not support @classmethod so this is needed.
	"""

	def __init__(self, anycallable):
		self.__call__ = anycallable

########################################################################
# HTTP post functionality for upload
########################################################################

class HTTP:
	
	def post_multipart(host, selector, fields, files):
    		"""
    		Post fields and files to an http host as multipart/form-data.
    		fields is a sequence of (name, value) elements for regular form fields.
    		files is a sequence of (name, filename, value) elements for data to be uploaded as files
    		Return the server's response page.
    		"""
    		content_type, body = HTTP.encode_multipart_formdata(fields, files)
    		h = httplib.HTTP(host)
    		h.putrequest('POST', selector)
    		h.putheader('content-type', content_type)
    		h.putheader('content-length', str(len(body)))
    		h.endheaders()
    		h.send(body)
    		errcode, errmsg, headers = h.getreply()
    		return h.file.read()

	post_multipart = Callable(post_multipart)

	def encode_multipart_formdata(fields, files):
    		"""
    		fields is a sequence of (name, value) elements for regular form fields.
    		files is a sequence of (name, filename, value) elements for data to be uploaded as files
    		Return (content_type, body) ready for httplib.HTTP instance
    		"""
    		BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
    		CRLF = '\r\n'
    		L = []
    		for (key, value) in fields.iteritems():
        		L.append('--' + BOUNDARY)
        		L.append('Content-Disposition: form-data; name="%s"' % key.encode("ascii", "replace"))
        		L.append('')
        		L.append(value.encode("ascii", "replace"))
    		for (key, filename, value) in files:
        		L.append('--' + BOUNDARY)
        		L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename.encode("ascii", "replace")))
        		L.append('Content-Type: "image/jpeg"')
        		L.append('')
        		L.append(value)
    		L.append('--' + BOUNDARY + '--')
    		L.append('')
    		body = CRLF.join(L)
    		content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
    		return content_type, body

	encode_multipart_formdata = Callable(encode_multipart_formdata)

########################################################################
# XML functionality
########################################################################

class XMLNode:
    'A parsed XML element'
    def __init__(self, name="", attrib={}):
        self.elementName = name
        self.elementText = ''
        self.attrib = attrib
        self.xml = ''

    #def __setitem__(self, key, item):
    #    self.attrib[key] = item

    def __getitem__(self, key):
        return self.attrib[key]
        
    def parseXML(xmlStr, storeXML=False):

        parser = Xml2Obj()
        rootNode = parser.Parse(xmlStr)
        if storeXML: rootNode.xml = xmlStr

        return rootNode

    parseXML = Callable(parseXML) # class method wrapper

class Xml2Obj:
    'XML to Object'
    def __init__(self):
        self.root = None
        self.nodeStack = []
        
    def StartElement(self, name, attributes):

        elementName = name.encode()
        
        'SAX start element even handler'
        # Instantiate an XMLNode object
        element = XMLNode(elementName, attributes)
        
        # Push element onto the stack and make it a child of parent
        # children are list attributes of a node
        if len(self.nodeStack) > 0:
            parent = self.nodeStack[-1]
            childList = None
            try:
                childList = getattr(parent, elementName)
                childList.append(element)
                setattr(parent, elementName, childList)
            except AttributeError:
                'The first child for the parent'
                setattr(parent, elementName, [element])
        else:
            self.root = element
        self.nodeStack.append(element)
        
    def EndElement(self,name):
        'SAX end element event handler'
        self.nodeStack = self.nodeStack[:-1]

    def CharacterData(self,data):
        'SAX character data event handler'
        if string.strip(data):
            data = data.encode("ascii", "replace")
            element = self.nodeStack[-1]
            element.elementText += data
            return

    def Parse(self,data):
        parser = xmllib.XMLParser()

        parser.unknown_starttag = self.StartElement
        parser.unknown_endtag = self.EndElement
        parser.handle_data = self.CharacterData

        parser.feed(data)
        parser.close()

        # Create a SAX parser
        #Parser = expat.ParserCreate()

        # SAX event handlers
        #Parser.StartElementHandler = self.StartElement
        #Parser.EndElementHandler = self.EndElement
        #Parser.CharacterDataHandler = self.CharacterData

        # Parse the XML File
        #ParserStatus = Parser.Parse(data, 1)
        
        return self.root

########################################################################
# Flickr functionality
########################################################################

#-----------------------------------------------------------------------
class FlickrAPI:
	"""Encapsulated flickr functionality.

	Example usage:

	  flickr = FlickrAPI(flickrAPIKey, flickrSecret)
	  rsp = flickr.auth_checkToken(api_key=flickrAPIKey, auth_token=token)

	"""
	flickrHost = "flickr.com"
	flickrRESTForm = "/services/rest/"
	flickrAuthForm = "/services/auth/"
	flickrUploadForm = "/services/upload/"

	#-------------------------------------------------------------------
	def __init__(self, apiKey, secret):
		"""Construct a new FlickrAPI instance for a given API key and secret."""
		self.apiKey = apiKey
		self.secret = secret
		self.username = None
		self.fullname = None

		self.__handlerCache={}

	#-------------------------------------------------------------------
	def __sign(self, data):
		"""Calculate the flickr signature for a set of params.

		data -- a hash of all the params and values to be hashed, e.g.
		        {"api_key":"AAAA", "auth_token":"TTTT"}

		"""
		dataName = self.secret
		keys = data.keys()
		keys.sort()
		for a in keys: dataName += (a + data[a])
		#print dataName
		hash = md5.new()
		hash.update(dataName)
		return hash.hexdigest()

	#-------------------------------------------------------------------
	def __getattr__(self, method, **arg):
		"""Handle all the flickr API calls.
		
		This is Michele Campeotto's cleverness, wherein he writes a
		general handler for methods not defined, and assumes they are
		flickr methods.  He then converts them to a form to be passed as
		the method= parameter, and goes from there.

		http://micampe.it/things/flickrclient

		My variant is the same basic thing, except it tracks if it has
		already created a handler for a specific call or not.

		example usage:

			flickr.auth_getFrob(api_key="AAAAAA")
			rsp = flickr.favorites_getList(api_key=flickrAPIKey, \\
				auth_token=token)

		"""

		if not self.__handlerCache.has_key(method):
			def handler(_self = self, _method = method, **arg):
				_method = "flickr." + _method.replace("_", ".")
				url = "http://" + FlickrAPI.flickrHost + \
					FlickrAPI.flickrRESTForm
				arg["method"] = _method
				postData = urllib.urlencode(arg) + "&api_sig=" + \
					_self.__sign(arg)
				#print "--url---------------------------------------------"
				#print url
				#print "--postData----------------------------------------"
				#print postData
				f = urllib.urlopen(url, postData)
				data = f.read()
				#print "--response----------------------------------------"
				#print data
				f.close()
				return XMLNode.parseXML(data, True)

			self.__handlerCache[method] = handler;

		return self.__handlerCache[method]
	
	#-------------------------------------------------------------------
	def __getAuthURL(self, perms, frob):
		"""Return the authorization URL to get a token.

		This is the URL the app will launch a browser toward if it
		needs a new token.
			
		perms -- "read", "write", or "delete"
		frob -- picked up from an earlier call to FlickrAPI.auth_getFrob()

		"""

		data = {"api_key": self.apiKey, "frob": frob, "perms": perms}
		data["api_sig"] = self.__sign(data)
		return "http://%s%s?%s" % (FlickrAPI.flickrHost, \
			FlickrAPI.flickrAuthForm, urllib.urlencode(data))

	#-------------------------------------------------------------------
	def upload(self, filename=None, jpegData=None, **arg):
		"""Upload a file to flickr.

		Be extra careful you spell the parameters correctly, or you will
		get a rather cryptic "Invalid Signature" error on the upload!

		Supported parameters:

		One of filename or jpegData must be specified by name when 
		calling this method:

		filename -- name of a file to upload
		jpegData -- array of jpeg data to upload

		api_key
		auth_token
		title
		description
		tags -- space-delimited list of tags, "tag1 tag2 tag3"
		is_public -- "1" or "0"
		is_friend -- "1" or "0"
		is_family -- "1" or "0"

		"""

		if filename == None and jpegData == None or \
			filename != None and jpegData != None:

			raise UploadException("filename OR jpegData must be specified")

		# verify key names
		for a in arg.keys():
			if a != "api_key" and a != "auth_token" and a != "title" and \
				a != "description" and a != "tags" and a != "is_public" and \
				a != "is_friend" and a != "is_family":

				sys.stderr.write("FlickrAPI: warning: unknown parameter " \
					"\"%s\" sent to FlickrAPI.upload\n" % (a))

		arg["api_sig"] = self.__sign(arg)
		url = "http://" + FlickrAPI.flickrHost + FlickrAPI.flickrUploadForm

		if filename != None:
			fp = file(filename, "rb")
			data = fp.read()
			fp.close()
		else:
			data = jpegData

		rspXML = HTTP.post_multipart(FlickrAPI.flickrHost,
						FlickrAPI.flickrUploadForm,
						arg, 
						[("photo", filename, data)])		
		return XMLNode.parseXML(rspXML)


	#-----------------------------------------------------------------------
	#@classmethod
	def testFailure(rsp, exit=True):
		"""Exit app if the rsp XMLNode indicates failure."""
		if rsp['stat'] == "fail":
			#sys.stderr.write("%s\n" % (cls.getPrintableError(rsp)))
			#if exit: sys.exit(1)
			raise FlickrException(FlickrAPI.getPrintableError(rsp))
	testFailure = Callable(testFailure)
	#-----------------------------------------------------------------------
	#@classmethod
	def getPrintableError(rsp):
		"""Return a printed error message string."""
		return "%s: error %s: %s" % (rsp.elementName, \
			FlickrAPI.getRspErrorCode(rsp), FlickrAPI.getRspErrorMsg(rsp))
	getPrintableError = Callable(getPrintableError)
	#-----------------------------------------------------------------------
	#@classmethod
	def getRspErrorCode(rsp):
		"""Return the error code of a response, or 0 if no error."""
		if rsp['stat'] == "fail":
			return rsp.err[0]['code']

		return 0
	getRspErrorCode = Callable(getRspErrorCode)
	#-----------------------------------------------------------------------
	#@classmethod
	def getRspErrorMsg(rsp):
		"""Return the error message of a response, or "Success" if no error."""
		if rsp['stat'] == "fail":
			return rsp.err[0]['msg']

		return "Success"
	getRspErrorMsg = Callable(getRspErrorMsg)
	#-----------------------------------------------------------------------
	def __getCachedTokenPath(self):
		"""Return the directory holding the app data."""
		#path = os.path.join(sys.path[0], "flickr", self.apiKey)
                path = APPPATH
                return unicode(path)

	#-----------------------------------------------------------------------
	def __getCachedTokenFilename(self):
		"""Return the full pathname of the cached token file."""
		filename = os.path.join(self.__getCachedTokenPath(), "auth.xml")
		return unicode(filename)

	#-----------------------------------------------------------------------
	def __getCachedToken(self):
		"""Read and return a cached token, or None if not found.

		The token is read from the cached token file, which is basically the
		entire RSP response containing the auth element.
		"""

		try:
			f = file(self.__getCachedTokenFilename(), "r")
			
			data = f.read()
			f.close()

			rsp = XMLNode.parseXML(data)
			return rsp.auth[0].token[0].elementText

		except IOError:
			return None

	#-----------------------------------------------------------------------
	def __setCachedToken(self, xml):
		"""Cache a token for later use.

		The cached tag is stored by simply saving the entire RSP response
		containing the auth element.

		"""

		path = self.__getCachedTokenPath()
		if not os.path.exists(path):
			os.makedirs(path)

		f = file(self.__getCachedTokenFilename(), "w")
		f.write(xml)
		f.close()


	#-----------------------------------------------------------------------
	def checkToken(self, perms="read"):
		"""Checks if the token exists in the cache and whether it is valid.
		If cached token is valid it is returned. Otherwise None is returned.
		"""
		
		# see if we have a saved token
		token = self.__getCachedToken()

                rsp = None

		# see if it's valid
		if token != None:
			rsp = self.auth_checkToken(api_key=self.apiKey, auth_token=token)
			if rsp['stat'] != "ok":
				token = None
			else:
				# see if we have enough permissions
				tokenPerms = rsp.auth[0].perms[0].elementText
				if tokenPerms == "read" and perms != "read": token = None
				elif tokenPerms == "write" and perms == "delete": token = None

		# if token is still valid
		if token != None:
                    # store user information
                    self.username = rsp.auth[0].user[0]['username']
                    self.fullname = rsp.auth[0].user[0]['fullname']

		return token
	
	#-----------------------------------------------------------------------
	def validate(self, perms="read"):
		"""Validates the application with given permissions.

		This function does not actually validate the application,
		but only returns the authentication URL that can then
		be shown to the user. This is done for implementations
		where browser cannot be launched from the Python.

		"""
		
		rsp = self.auth_getFrob(api_key=self.apiKey)
		self.testFailure(rsp)
		frob = rsp.frob[0].elementText
		return frob, self.__getAuthURL(perms, frob)
	
	#-----------------------------------------------------------------------
	def getToken(self, frob):
		"""Gets a token for given frob and stores it in the cache for
		future use.

		"""
		
		rsp = self.auth_getToken(api_key=self.apiKey, frob=frob)
		self.testFailure(rsp)
		token = rsp.auth[0].token[0].elementText

                # store user information
		self.username = rsp.auth[0].user[0]['username']
		self.fullname = rsp.auth[0].user[0]['fullname']
		
		# store the auth info for next time
		self.__setCachedToken(rsp.xml)

		return token

	#-----------------------------------------------------------------------
	def getFullToken(self, miniToken):
		"""Exchanges the mini-token for a full token. This is needed
		for the mobile applications.

		Returns the token with the flickr username and fullname of the
		authenticated user.
		"""

		data = {"api_key" : self.apiKey,
			"method" : "flickr.auth.getFullToken",
			"mini_token" : miniToken}

		rsp = self.auth_getFullToken(api_key=self.apiKey,
					     mini_token=miniToken)
		self.testFailure(rsp)
		token = rsp.auth[0].token[0].elementText

                # store user information
		self.username = rsp.auth[0].user[0]['username']
		self.fullname = rsp.auth[0].user[0]['fullname']

		# cache the token for future use
		self.__setCachedToken(rsp.xml)

		return token
		
########################################################################
# App functionality
########################################################################

def main(argv):

	# flickr auth information:
	flickrAPIKey = ""  # API key
	flickrSecret = ""  # shared "secret"

	# make a new FlickrAPI instance
	fapi = FlickrAPI(flickrAPIKey, flickrSecret)

	# check if you already have proper token cached
	token = fapi.checkToken(perms="read")

	if token:
		pass # everythin ok
	else:
		# ask user to give the minitoken
		#miniToken = raw_input("Give the minitoken: ")
		miniToken = appuifw.query(u'Give the mini-token:', 'text')
		token = fapi.getFullToken(miniToken)
		
	# your favourites
	rsp = fapi.favorites_getList(api_key=flickrAPIKey,auth_token=token)
	fapi.testFailure(rsp)

	# and print them
	for a in rsp.photos[0].photo:
		print "%10s: %s" % (a['id'], a['title'].encode("ascii", "replace"))
	

	# upload the file foo.jpg
	rsp = fapi.upload(filename="e:\\Images\\test.jpg", api_key=flickrAPIKey, auth_token=token, 
		title="This is the title", description="This is the description",
		tags="tag1 tag2 tag3", is_public="1")
	if rsp == None:
		sys.stderr.write("can't find file\n")
	else:
		fapi.testFailure(rsp)
		print "Photo uploaded..."
	return 0

# run the main if we're not being imported:
if __name__ == "__main__": sys.exit(main(sys.argv))

